「出かけようとしたら雨が降ってた……」を防ぐため、「1時間毎の天気予報」を1時間毎にSlack通知してみた
天気予報を毎日見ていますか? 私は見ていません。そのせいで「夕方になったら雨が降ってきた……。スーパーは昼に行けばよかった……。」といった後悔は1度や2度ではありません。そこで考えました。天気予報をSlackに通知すれば否が応でも見るだろう、と。
通知内容と頻度
- 通知内容
- 今日の天気:1時間ごと(12時間分)
- 週間の天気:1日ごと(8日分)
- 通知頻度
- 毎日0時から1時間毎
実際の様子がこちらです。
システム概要
超シンプルなサーバーレス構成です。
通知先の準備
Slackチャンネルを作成
notify-weather-tokyo
チャンネルを作成しました。
Slackアプリを追加
https://api.slack.com/apps にアクセスし、Slackアプリを作成します。
Incoming Webhooksを有効化
Incoming Webhooks
設定をONにします。
Incoming Webhookの作成 & 通知先チャンネル設定
先ほど作成したチャンネルを通知先に設定します。
設定完了です。URLはあとでパラメータストアに登録します。
OpenWeatherのAPIキーを取得
OpenWeatherMapにサインアップし、APIキーを取得します。
サーバーレスアプリの作成
パラメータストアに値を追加
AWS Systems Managerのパラメータストアに次の値を追加します。
- 緯度
- 経度
- Slackの通知先URL
- OpenWeatherMapのAPIキー
緯度経度は東京を設定しています。
緯度
aws ssm put-parameter \ --type 'String' \ --name '/Notify-Weather-To-Slack-App/tokyo/Latitude' \ --value '35.681236'
経度
aws ssm put-parameter \ --type 'String' \ --name '/Notify-Weather-To-Slack-App/tokyo/Longitude' \ --value '139.767125'
Slackの通知先URL
URLの先頭にhttps://
があると、コマンド実行が失敗するため取り除いています。
aws ssm put-parameter \ --type 'String' \ --name '/Notify-Weather-To-Slack-App/tokyo/slack_url' \ --value 'hooks.slack.com/services/xxxxx/yyyyy/zzzzz'
OpenWeatherのAPIキー
aws ssm put-parameter \ --type 'String' \ --name '/Notify-Weather-To-Slack-App/apikey' \ --value 'xxxx'
AWS SAMプロジェクトの作成
sam init --runtime python3.7 --name Notify-Weather-To-Slack-App
テンプレートファイル
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: Notify-Weather-To-Slack-App Parameters: TargetName: Type: String Latitude: Type: AWS::SSM::Parameter::Value<String> Longitude: Type: AWS::SSM::Parameter::Value<String> ApiKey: Type: AWS::SSM::Parameter::Value<String> SlackUrl: Type: AWS::SSM::Parameter::Value<String> Resources: NotifyWeatherFunction: Type: AWS::Serverless::Function Properties: FunctionName: !Sub notify-weather-to-slack-${TargetName}-function CodeUri: src/ Handler: app.lambda_handler Runtime: python3.7 Timeout: 10 Environment: Variables: LATITUDE: !Ref Latitude LONGITUDE: !Ref Longitude API_KEY: !Ref ApiKey SLACK_URL: !Ref SlackUrl Events: NotifySlack: Type: Schedule Properties: Schedule: cron(0 0/1 * * ? *) # 日本時間で毎日0時から1時間毎 NotifyWeatherFunctionLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/lambda/${NotifyWeatherFunction}"
Lambdaコード
import json import locale import os import requests from datetime import datetime, timezone, timedelta LATITUDE = os.environ['LATITUDE'] LONGITUDE = os.environ['LONGITUDE'] API_KEY = os.environ['API_KEY'] SLACk_URL = os.environ['SLACK_URL'] def lambda_handler(event, context): main() def main(): # 曜日表記を日本語にする locale.setlocale(locale.LC_TIME, 'ja_JP.UTF-8') # 気象情報を取得する weather_endpoint = get_weather_endpoint(LATITUDE, LONGITUDE, API_KEY) weather_data = get_weather_data(weather_endpoint) # Slack通知用のメッセージを作成する message = create_message(weather_data) print(json.dumps(message)) # Slackに通知する post_slack(message) def get_weather_endpoint(latitude, longitude, api_key): base_url = 'https://api.openweathermap.org/data/2.5/onecall' return f'{base_url}?lat={latitude}&lon={longitude}&exclude=current&units=metric&lang=ja&appid={api_key}' def get_weather_data(endpoint): res = requests.get(endpoint) return res.json() def convert_unixtime_to_jst_datetime(unixtime): return datetime.fromtimestamp(unixtime, timezone(timedelta(hours=9))) def get_icon_url(icon_name): return f'http://openweathermap.org/img/wn/{icon_name}@2x.png' def create_message(weather_data): hourly = create_message_blocks_hourly(weather_data) daily = create_message_blocks_daily(weather_data) message_blocks = [] message_blocks += hourly message_blocks.append({ 'type': 'divider' }) message_blocks += daily return { 'blocks': message_blocks } def create_message_blocks_hourly(weather_data): message_blocks = [] hourly = weather_data['hourly'] # 見出しを作る first_datetime = convert_unixtime_to_jst_datetime(hourly[0]['dt']) message_blocks.append({ 'type': 'section', 'text': { 'type': 'plain_text', 'text': first_datetime.strftime('%m/%d(%a)') } }) # 1時間毎のメッセージを作る(12時間分) for i in range(12): item = hourly[i] target_datetime = convert_unixtime_to_jst_datetime(item['dt']) description = item['weather'][0]['description'] icon_url = get_icon_url(item['weather'][0]['icon']) rain = item.get('rain', {'1h': 0}).get('1h') # rainが無い場合は0mm/hとする temperature = item['temp'] humidity = item['humidity'] pressure = item['pressure'] wind_speed = item['wind_speed'] message_blocks.append({ 'type': 'context', 'elements': [ { 'type': 'mrkdwn', 'text': target_datetime.strftime('%H:%M') }, { 'type': 'image', 'image_url': icon_url, 'alt_text': description }, { 'type': 'mrkdwn', 'text': f'{description: <6} {rain:>5.1f}mm/h {temperature:>4.1f}℃ ' f'{humidity}% {pressure:>4}hPa {wind_speed:>5.1f}m/s' } ] }) return message_blocks def create_message_blocks_daily(weather_data): message_blocks = [] daily = weather_data['daily'] # 見出しを作る message_blocks.append({ 'type': 'section', 'text': { 'type': 'plain_text', 'text': '週間天気' } }) # 1日毎のメッセージを作る for item in daily: target_datetime = convert_unixtime_to_jst_datetime(item['dt']) description = item['weather'][0]['description'] icon_url = get_icon_url(item['weather'][0]['icon']) rain = item.get('rain', 0) # rainが無い場合は0mm/hとする temperature_min = item['temp']['min'] temperature_max = item['temp']['max'] humidity = item['humidity'] pressure = item['pressure'] wind_speed = item['wind_speed'] message_blocks.append({ 'type': 'context', 'elements': [ { 'type': 'mrkdwn', 'text': target_datetime.strftime('%m/%d(%a)') }, { 'type': 'image', 'image_url': icon_url, 'alt_text': description }, { 'type': 'mrkdwn', 'text': f'{description: <6} {rain:>5.1f}mm/h {temperature_min:>4.1f} - {temperature_max:>4.1f}℃ ' f'{humidity}% {pressure:>4}hPa {wind_speed:>5.1f}m/s' } ] }) return message_blocks def post_slack(payload): url = f'https://{SLACk_URL}' try: response = requests.post(url, data=json.dumps(payload)) except requests.exceptions.RequestException as e: print(e) else: print(response.status_code)
デプロイ
parameter-overrides
オプションでパラメータストアの情報を渡しています。
sam build sam package \ --output-template-file packaged.yaml \ --s3-bucket your-bucket-name sam deploy \ --template-file packaged.yaml \ --stack-name Notify-Weather-To-Slack-App-tokyo \ --capabilities CAPABILITY_NAMED_IAM \ --no-fail-on-empty-changeset \ --parameter-overrides \ TargetName=tokyo \ Latitude=/Notify-Weather-To-Slack-App/tokyo/Latitude \ Longitude=/Notify-Weather-To-Slack-App/tokyo/Longitude \ ApiKey=/Notify-Weather-To-Slack-App/apikey \ SlackUrl=/Notify-Weather-To-Slack-App/tokyo/slack_url
動作確認
しばらくすると……、無事に通知がきました!!
さいごに
天気予報をSlackに通知してみました。これで天気予報を見る習慣ができるでしょう。